package com.google.code.b0rx0r.ui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Shape;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import com.google.code.b0rx0r.ColorScheme;
import com.google.code.b0rx0r.PlayQueue;
import com.google.code.b0rx0r.program.OffsetSampleDataContainer;
import com.google.code.b0rx0r.program.SampleDataContainer;
import com.google.code.b0rx0r.program.Sequencable;
import com.google.code.b0rx0r.program.Sequence;
import com.google.code.b0rx0r.program.SequenceListener;
import com.google.code.b0rx0r.program.SequencedSequencable;
import com.google.code.b0rx0r.program.SnapPoint;

public class Sequencer extends BasicComponent implements SequenceListener {
	private Sequence sequence;
	private Dimension dim = new Dimension(600, 100);
	
	private SequencedSequencable selected;
	
	
	public Sequencer(MainPane main) {
		super(main);
		initGui();
	}

	
	public void setSequence(Sequence sequence) {
		
		if (this.sequence != null) {
			this.sequence.removeSequenceListener(this);
		}
		
		this.sequence = sequence;
		sequence.addSequenceListener(this);
	}
	
	
	private SequencedSequencable hovering;
	private SequencedSequencable dragging;
	private Point draggingOldPos;
	
	
	private float getSamplesPerPixel() { //TODO deal with zoom
		return (float)sequence.getLength() / getWidth();
	}
	
	private void initGui() {
		MouseAdapter ma = new MouseAdapter() {

			@Override
			public void mouseMoved(MouseEvent e) {
				SequencedSequencable ss = findSequencedSequencable(e.getPoint());
				if (ss != hovering) {
					hovering = ss;
					repaint();
				}
			}


			
			@Override
			public void mousePressed(MouseEvent e) {
				if (e.getButton() == MouseEvent.BUTTON1) {
					SequencedSequencable ss = findSequencedSequencable(e.getPoint());
					draggingOldPos = e.getPoint();
					if (ss != dragging) {
						dragging = ss;
						repaint();
					}
				}
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				if (e.getButton() == MouseEvent.BUTTON1) {
					if (dragging == selected)
						selected = null;
					else
					  selected = dragging;
					
					dragging = null;
					draggingOldPos = null;
					snappedSnapPoint.clear();
					repaint();
					setHint(null);
				}
			}

			@Override
			public void mouseDragged(MouseEvent e) {
				Point p = e.getPoint();
				if (draggingOldPos != null) {
					
					int track = e.getY() / 24;
					dragging.setTrack(track);
					
					int diffX = p.x - draggingOldPos.x;
					dragging.setStartOffset(dragging.getStartOffset() + (int)(diffX * getSamplesPerPixel())); //TODO respect zoom

					boolean snap = ((e.getModifiers() & MouseEvent.ALT_MASK) == 0);

					if (snap) {
					  snap(dragging);
					  setHint(Messages.getString("Sequencer.HintDisableSnapping")); //$NON-NLS-1$
					}
					else {
						snappedSnapPoint.clear();
						setHint(null);
					}
					
					repaint();
				}
				draggingOldPos = p;
			}
			
		};
		
		addMouseListener(ma);
		addMouseMotionListener(ma);
		
		setDropTarget(new DropTarget(this, new DropTargetListener() {

			@Override
			public void dragEnter(DropTargetDragEvent dtde) {
				if (dtde.isDataFlavorSupported(Sequencable.DATA_FLAVOR))
  				try {
						Sequencable sequencable = (Sequencable)dtde.getTransferable().getTransferData(Sequencable.DATA_FLAVOR);
						if (sequencable == sequence) {
							dtde.rejectDrag();
						} else {
 						  dtde.acceptDrag(DnDConstants.ACTION_MOVE);
						}
  				} catch (UnsupportedFlavorException e) {
						e.printStackTrace(); 
  				} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				
			}

			@Override
			public void dragExit(DropTargetEvent dte) {
				// TODO Auto-generated method stub
				
			}

			@Override
			public void dragOver(DropTargetDragEvent dtde) {

				
			}

			@Override
			public void drop(DropTargetDropEvent dtde) {
				if (dtde.isDataFlavorSupported(Sequencable.DATA_FLAVOR)) {
					dtde.acceptDrop(DnDConstants.ACTION_MOVE);
					
					Point point = dtde.getLocation();
					int offset = getOffset(point); 
					int track = getTrack(point);
					Sequencable sequencable;

					boolean success = false;
					
					try {
						sequencable = (Sequencable)dtde.getTransferable().getTransferData(Sequencable.DATA_FLAVOR);
						
						//TODO: also prevent recursion over multiple steps!
						if (sequence == sequencable) {
							dtde.dropComplete(false);
							return;
						}
						
						sequence.addSequencable(sequencable, track, offset);
						success = true;
					} catch (UnsupportedFlavorException e) {
						e.printStackTrace();
					} catch (IOException e) {
						e.printStackTrace();
					}
					
				  dtde.dropComplete(success);
				}
				
				repaint();//TODO replace with listener on sequence
			}

			@Override
			public void dropActionChanged(DropTargetDragEvent dtde) {
				// TODO Auto-generated method stub
				
			}
			
		}));
	}
	
	private Set<SnapPoint> snappedSnapPoint = new HashSet<SnapPoint>();
	
	
	protected void snap(SequencedSequencable ss) {
		SnapPoint bestPairSS = null;
		SnapPoint bestPairSeq = null;
		int bestPairDifference = 0;
		int bestPairAbsoluteDifference = Integer.MAX_VALUE;
		
		// find the pair (snappoint in ss, snappoint in seq) with the smallest absolute difference
		for (SnapPoint spSS : ss.getSequencable().getRelativeSnapPoints()) {
			for (SnapPoint spSeq : sequence.getRelativeSnapPoints()) {
				int offsetSS = spSS.getOffset() + ss.getStartOffset();
				int offsetSeq = spSeq.getOffset();
				
				
				int diff = offsetSeq - offsetSS;
				int diffAbs = Math.abs(diff);
				
				if (diffAbs < bestPairAbsoluteDifference) {
					bestPairAbsoluteDifference = diffAbs;
					bestPairDifference = diff;
					bestPairSS = spSS;
					bestPairSeq = spSeq;
				}
			}
		}
		
		snappedSnapPoint.clear();
		// if the absolute difference is smaller than the snap offset, snap
		if (bestPairAbsoluteDifference < 1000) { //TODO make snap offset (in Samples!!) a const
			ss.setStartOffset(ss.getStartOffset() + bestPairDifference);

			snappedSnapPoint.add(bestPairSS);
			snappedSnapPoint.add(bestPairSeq);
		}
		
	}


	private SequencedSequencable findSequencedSequencable(Point point) {
		int track = getTrack(point);
		int offset = getOffset(point);
		
		for (SequencedSequencable ss : sequence) {
			if (ss.getTrack() == track &&
					ss.getStartOffset() <= offset &&
					ss.getStartOffset()+ ss.getSequencable().getLength() >= offset) {
				return ss;
			}
		}

		return null;
	}


	private int getOffset(Point point) {
		return (int)(point.x * getSamplesPerPixel()); 
	}


	private Dimension getDimension() {
		dim.width = 600;
		
		if (sequence != null)
		  dim.height = sequence.getTrackCount() * 24;
		
		return dim;
	}

	@Override
	public Dimension getMaximumSize() {
		return getDimension();
	}

	@Override
	public Dimension getMinimumSize() {
		return getDimension();
	}

	@Override
	public Dimension getPreferredSize() {
		return getDimension();
	}

	
	
	
	
	@Override
	public void paint(Graphics g) {
		super.paint(g);
		
		float samplesPerPixel = getSamplesPerPixel();
		
		if (sequence != null) {
			paintTracks(g);
			paintSequencables(g, samplesPerPixel);
			paintSnapPoints(g, samplesPerPixel);
			paintBeatsAndBars(g);
		}
	}

	private void paintTracks(Graphics g) {
		for (int i=0; i<sequence.getTrackCount(); i++) {
			paintTrack(i, g);
		}

	}
	
	private void paintSequencables(Graphics g, float samplesPerPixel) {
		for (SequencedSequencable s : sequence) {
			paintSequencable(s, g, samplesPerPixel);
		}
	}

  private SimpleWaveformDisplay swd = new SimpleWaveformDisplay(100,22);
  
	private void paintSequencable(SequencedSequencable s, Graphics g, float samplesPerPixel) {
		Sequencable se = s.getSequencable();
		
		int x0 = (int)(s.getStartOffset()/samplesPerPixel) ;
		int y0 = s.getTrack() * 24 +1;
		int width = (int)(s.getSequencable().getLength() / samplesPerPixel);

		Shape clip = g.getClip();
		g.setClip(x0,y0,width,22);


		g.setColor(s == dragging || s == selected ? se.getColor().brighter() : se.getColor());
		g.fillRect(x0, y0, width, 22);
		
		g.setColor(s == hovering ? ColorScheme.SEQUENCER_SEQUENCABLE_HOVERING_LINE_COLOR : ColorScheme.SEQUENCER_SEQUENCABLE_LINE_COLOR);
		g.drawRect(x0, y0, width, 22);
		
		g.setColor(ColorScheme.SEQUENCER_SEQUENCABLE_WAVEFORM_COLOR);
		
		for (OffsetSampleDataContainer osdc : se.getSampleData()) {
			SampleDataContainer sdc = osdc.getSampleDataContainer();
			swd.setSize(width, 22);
			swd.setWidth(width);
			swd.setOpaque(false);
			swd.setData(sdc.getFloatSampleBuffer(), sdc.getStartOffset(), sdc.getEndOffset() - sdc.getStartOffset());
			swd.paintComponent(g.create((int)(x0 + (osdc.getOffset() ) / samplesPerPixel), 
					               y0, 
					               width, 
					               22));
		}

		g.setColor(ColorScheme.SEQUENCER_SEQUENCABLE_TEXT_COLOR);
		if (se.getName() != null)
			g.drawString(se.getName(), x0+2, y0+18);
		
		for (SnapPoint sp : s.getSequencable().getRelativeSnapPoints()) {
			g.setColor(snappedSnapPoint.contains(sp) ? ColorScheme.SEQUENCER_SEQUENCABLE_SNAPPOINT_SNAPPED_COLOR : ColorScheme.SEQUENCER_SEQUENCABLE_SNAPPOINT_COLOR);
			int x = x0+(int)(sp.getOffset()/samplesPerPixel);
			g.drawLine(x, y0, 
                 x, y0+22);
		}
		
		g.setClip(clip);
	}
	
	private void paintSnapPoints(Graphics g, float samplesPerPixel) {
		for (SnapPoint sp : sequence.getRelativeSnapPoints()) {
			g.setColor(snappedSnapPoint.contains(sp) ? ColorScheme.SEQUENCER_SNAPPOINT_SNAPPED_COLOR : ColorScheme.SEQUENCER_SNAPPOINT_COLOR);
			int x = (int)(sp.getOffset()/samplesPerPixel);
			g.drawLine(x, 0, 
                 x, g.getClipBounds().height);
		}
	}
	private void paintBeatsAndBars(Graphics g) {
		int beats = sequence.getLengthBars() * sequence.getBeatsPerBar();
		float pixelsPerBeat = ((float)g.getClipBounds().width)/beats;
		
		for (int i=0; i < beats; i++) {
			int x = (int)(i * pixelsPerBeat);
			
			Color c = i % sequence.getBeatsPerBar() == 0 ? ColorScheme.SEQUENCER_BEAT_BAR_COLOR : ColorScheme.SEQUENCER_BEAT_COLOR;
			
			g.setColor(c);
			g.drawLine(x, 0, x, g.getClipBounds().height);
		}
		
	}


	private void paintTrack(int nr, Graphics g) {
		int y0 = nr * 24;
		
		g.setColor(ColorScheme.SEQUENCER_TRACK_FILL_COLOR);
		g.fillRect(0, y0, g.getClipBounds().width, 24);
	
		g.setColor(ColorScheme.SEQUENCER_TRACK_LINE_COLOR);
		g.drawRect(0, y0, g.getClipBounds().width, 24);

		
	}


	private int getTrack(Point point) {
		int track = point.y/24;
		return track;
	}


	@Override
	public void sequenceParametersChanged() {
		repaint();
	}


	@Override
	public void snapPointsChanged() {
		repaint();
	}


	@Override
	public void tracksChanged() {
		// TODO Auto-generated method stub
		
	}


	public void triggerPreview(PlayQueue pq) {
		if (selected != null)
			pq.enqueueSequencable(selected.getSequencable(), 127);
		
		else if (sequence != null)
			pq.enqueueSequencable(sequence, 127);
	}

	

	
	
}
